/*
 * $Id: QEADIFImporter.m 90 2009-07-16 03:06:57Z jon@bway.net $
 * Chalk
 *
 * Created 6/23/09
 * Copyright Jon Gordon
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#import "QEADIFImporter.h"

#import "QEADIF.h"

typedef enum {
    qeKeyColumnInt16 = 1,
    qeKeyColumnInt32,
    qeKeyColumnInt64,
    qeKeyColumnDecimal,
    qeKeyColumnDouble,
    qeKeyColumnFloat,
    qeKeyColumnString,
    qeKeyColumnBoolean,
    qeKeyColumnDate,
    qeKeyColumnBinaryData,
    qeKeyColumnBooleanQslSent = 129,
    qeKeyColumnBooleanQslRcvd
} qeADIFColumnType;

@interface QEADIFImporter (private)
+ (void)initializeTables;
+ (void)initializeField2ColumnMap;
+ (void)initializeColumn2FieldsMap;
+ (void)initializeColumnTypeTable;
@end


static NSDictionary *field2ColumnMap;
static NSDictionary *column2FieldsMap;
static NSDictionary *columnTypeTable;
static NSSet *callsignColumns;
static NSCalendar *gregorianCalendar;

static NSSet *qslSentFields;
static NSSet *qslRcvdFields;
static NSSet *allQslFields;

static const NSUInteger DATE_COMPONENTS = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
static const NSUInteger TIME_COMPONENTS = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;


@implementation QEADIFImporter
#pragma mark LIFECYCLE
+ (void)initialize
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    static BOOL initialized = NO;
    
    if (!initialized) {
        //-NSLog(@"  initializing");
        initialized = YES;
        callsignColumns = [[NSSet alloc] initWithObjects:@"callsign", nil];
        gregorianCalendar = [[NSCalendar alloc]
                             initWithCalendarIdentifier:NSGregorianCalendar];
        [gregorianCalendar setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
        
        qslSentFields = [[NSSet alloc] initWithObjects:@"EQSL_QSL_SENT", @"LOTW_QSL_SENT", @"QSL_SENT", nil];
        qslRcvdFields = [[NSSet alloc] initWithObjects:@"EQSL_QSL_RCVD", @"LOTW_QSL_RCVD", @"QSL_RCVD", nil];
        allQslFields = [[qslSentFields setByAddingObjectsFromSet:qslRcvdFields] retain];
        
        [QEADIFImporter initializeTables];
    } // if
} // initialize

- (id)init
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    self = [super init];
    
    if (nil != self) {
        tmpRecord = [[NSMutableDictionary alloc] init];
        preservedFields = [[NSMutableArray alloc] init];
    } // if
    
    return self;
} // init

- (void)dealloc
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [tmpRecord release];
    [preservedFields release];
    [super dealloc];
} // dealloc


#pragma mark OPERATIONS
- (NSDictionary *)importADIFRecord:(NSArray *)record
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [NSDictionary dictionaryWithDictionary:[self importADIFRecord:record intoObject:nil]];
} // importADIFRecord:

- (id)importADIFRecord:(NSArray *)record intoObject:(id)dest
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [tmpRecord removeAllObjects];
    [preservedFields removeAllObjects];
    
    NSMutableSet *dateColumns = [[NSMutableSet alloc] init];
    
    if (nil == record) {
        return nil;
    } // if
    
    id result = dest;
    if (nil == result) {
        //-NSLog(@"  creating new mutable dictionary to hold result");
        result = [[[NSMutableDictionary alloc] initWithCapacity:[record count]] autorelease];
    } // if
    
    for (QEADIFDataField *field in record) {
        //-NSLog(@"  importing field: %@", field);
        NSString *columnName = [field2ColumnMap objectForKey:[field fieldName]];
        if (nil == columnName) {
            //-NSLog(@"  field name \"%@\" does not map to a column", [field fieldName]);
            [preservedFields addObject:field];
        } else {
            //-NSLog(@"  mapping to column \"%@\"", columnName);
            switch ([[columnTypeTable objectForKey:columnName] integerValue]) {
                case qeKeyColumnInt16:
                case qeKeyColumnInt32:
                case qeKeyColumnInt64:
                    [tmpRecord setObject:[NSNumber numberWithInt:[[field fieldValue] integerValue]] forKey:columnName];
                    break;
                case qeKeyColumnDecimal:
                    [tmpRecord setObject:[[field fieldValue] decimalValue] forKey:columnName];
                    break;
                case qeKeyColumnDouble:
                    [tmpRecord setObject:[NSNumber numberWithDouble:[[[field fieldValue] decimalValue] doubleValue]]
                                  forKey:columnName];
                    break;
                case qeKeyColumnFloat:
                    [tmpRecord setObject:[NSNumber numberWithFloat:[[[field fieldValue] decimalValue] floatValue]]
                                  forKey:columnName];
                    break;
                case qeKeyColumnString:
                    if (nil == [callsignColumns member:columnName]) {
                        [tmpRecord setObject:[[field fieldValue] stringValue] forKey:columnName];
                    } else {
                        [tmpRecord setObject:[[[field fieldValue] stringValue] uppercaseString] forKey:columnName];
                    } // else
                    break;
                case qeKeyColumnBoolean:
                    [tmpRecord setObject:[NSNumber numberWithBool:[[field fieldValue] boolValue]]
                                  forKey:columnName];
                    break;
                case qeKeyColumnBinaryData:
                    [tmpRecord setObject:[[field fieldValue] value] forKey:columnName];
                    break;
                case qeKeyColumnDate:
                    if (nil == [dateColumns member:columnName]) {
                        [dateColumns addObject:columnName];
                        [tmpRecord setObject:[[[[field fieldValue] componentValue] copy] autorelease]
                                      forKey:columnName];
                    } else {
                        NSDateComponents *c = [tmpRecord objectForKey:columnName];
                        NSDateComponents *newC = [[field fieldValue] componentValue];
                        if (qeADIFTypeDate == [QEADIFValue typeForFieldName:[field fieldName]]) {
                            [c setYear:[newC year]];
                            [c setMonth:[newC month]];
                            [c setDay:[newC day]];
                        } else if (qeADIFTypeTime == [QEADIFValue typeForFieldName:[field fieldName]]) {
                            [c setHour:[newC hour]];
                            [c setMinute:[newC minute]];
                            [c setSecond:[newC second]];
                        } else {
                            // CAN'T HAPPEN
                            //-NSLog(@"  ERROR: trying to store field \"%@\" into column \"%@\"; field isn't date or time, but column requires components", \
                                  [field fieldName], \
                                  columnName);
                        } //else
                    } // else
                    break;
                case qeKeyColumnBooleanQslRcvd:
                    if ([@"Y" isEqualToString:[[field fieldValue] stringValue]] ||
                        [@"N" isEqualToString:[[field fieldValue] stringValue]]) {
                        [tmpRecord setObject:[NSNumber numberWithBool:[[field fieldValue] boolValue]]
                                      forKey:columnName];
                    } else {
                        // record in entity that we haven't yet received QSL,
                        // but preserve the additional information that ADIF
                        // may include
                        [tmpRecord setObject:[NSNumber numberWithBool:NO] forKey:columnName];
                        [preservedFields addObject:field];
                    } // else
                    break;
                case qeKeyColumnBooleanQslSent:
                    if ([@"Y" isEqualToString:[[field fieldValue] stringValue]] ||
                        [@"N" isEqualToString:[[field fieldValue] stringValue]]) {
                        [tmpRecord setObject:[NSNumber numberWithBool:[[field fieldValue] boolValue]]
                                      forKey:columnName];
                    } else {
                        // record in entity that we haven't yet received QSL,
                        // but preserve the additional information that ADIF
                        // may include
                        [tmpRecord setObject:[NSNumber numberWithBool:NO] forKey:columnName];
                        [preservedFields addObject:field];
                    } // else
                    break;
                default:
                    //-NSLog(@"  no column type known for column \"%@\"; treating as a string", \
                          columnName);
                    [tmpRecord setObject:[[field fieldValue] stringValue] forKey:columnName];
                    break;
            } // switch
        } // else
    } // for
    
    for (NSString *dateColumnName in dateColumns) {
        //-NSLog(@"  converting date column \"%@\"", dateColumnName);
        NSDateComponents *c = [tmpRecord objectForKey:dateColumnName];
        [tmpRecord setObject:[gregorianCalendar dateFromComponents:c] forKey:dateColumnName];
    } // for
    
    if (0 < [preservedFields count]) {
        NSMutableString *preservedFieldText = [[NSMutableString alloc] init];
        for (QEADIFDataField *preservedField in preservedFields) {
            [preservedFieldText appendString:[preservedField description]];
        } // for
        [tmpRecord setValue:preservedFieldText forKey:@"preservedFields"];
        [preservedFieldText release];
    } // if
    
    NSArray *columnNames = [tmpRecord allKeys];
    for (id columnName in columnNames) {
        //-NSLog(@"  saving value \"%@\" for column: %@", [tmpRecord valueForKey:columnName], columnName);
        [result setValue:[tmpRecord valueForKey:columnName] forKey:columnName];
    } // for
    //-NSLog(@"  all values stored, returning: %@", result);
    
    return result;
} // importADIFRecord:intoObject:


- (BOOL)exportAsADIF:(NSArray *)records toFilename:(NSString *)filename error:(NSError **)outError
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == records) {
        [[NSException exceptionWithName:NSInvalidArgumentException
                                 reason:@"Nil array of records provided for ADIF export"
                               userInfo:nil] raise];
        return NO;
    } // if
    
    if (nil == filename) {
        [[NSException exceptionWithName:NSInvalidArgumentException
                                 reason:@"Nil filename provided for ADIF export"
                               userInfo:nil] raise];
        return NO;
    } // if
    
    // create file and fill data
    QEADIFFile *outFile = [[QEADIFFile alloc] init];
    NSMutableArray *exportRecords = [NSMutableArray arrayWithCapacity:[records count]];
    
    for (NSManagedObject *record in records) {
        NSMutableDictionary *fields = nil;
        NSString *preservedFieldSrc = [record valueForKey:@"preservedFields"];
        if (nil != preservedFieldSrc) {
            fields = [NSMutableDictionary 
                      dictionaryWithDictionary:[QEADIFFileMaker makeADIFRecordFromSource:preservedFieldSrc]];
        } // if
        
        if (nil == fields) {
            fields = [[[NSMutableDictionary alloc] init] autorelease];
        } // if
        
        NSArray *colNames = [[[record entity] attributesByName] allKeys];
        for (NSString *colName in colNames) {
            id value = [record valueForKey:colName];
            if (nil ==value) {
                continue;
            } // if
            
            //-NSLog(@"  colName: %@; value: %@", colName, value);
            NSArray *fieldNames = [column2FieldsMap objectForKey:colName];
            //-NSLog(@"  mapping to column(s): %@", fieldNames);
            if (nil == fieldNames) {
                continue;
            } // if
            
            for (NSString *fieldName in fieldNames) {
                QEADIFType dataType = [QEADIFValue typeForFieldName:fieldName];
                NSAssert1(qeADIFTypeUndefined != dataType,
                          @"data type for ADIF field \"%@\" must be defined",
                          fieldName);
                id enumSpec = nil;
                if (qeADIFTypeEnumeration == dataType) {
                    enumSpec = [QEADIFValue enumerationSpecForFieldName:fieldName];
                    NSAssert1(nil != enumSpec,
                              @"  enumeration specifier for field name \"%@\" is nil",
                              fieldName);
                } // if
                
                QEADIFValue *fieldValue = nil;
                switch (dataType) {
                    case qeADIFTypeAwardList:
                        fieldValue = [QEADIFValue awardListFromString:value];
                        break;
                    case qeADIFTypeBoolean:
                        fieldValue = [QEADIFValue booleanFromBool:[value boolValue]];
                        break;
                    case qeADIFTypeDigit:
                        fieldValue = [QEADIFValue digitFromString:[value stringValue]];
                        break;
                    case qeADIFTypeNumber:
                        fieldValue = [QEADIFValue numberFromString:[value stringValue]];
                        break;
                    case qeADIFTypeDate:
                        fieldValue = [QEADIFValue 
                                      dateFromComponents:[gregorianCalendar 
                                                          components:DATE_COMPONENTS 
                                                          fromDate:value]];
                        break;
                    case qeADIFTypeTime:
                        fieldValue = [QEADIFValue 
                                      timeFromComponents:[gregorianCalendar 
                                                          components:TIME_COMPONENTS 
                                                          fromDate:value]];
                        break;
                    case qeADIFTypeCharacter:
                        fieldValue = [QEADIFValue characterFromString:value];
                        break;
                    case qeADIFTypeString:
                        fieldValue = [QEADIFValue stringFromString:value];
                        break;
                    case qeADIFTypeMultilineString:
                        fieldValue = [QEADIFValue multilineStringFromString:value];
                        break;
                    case qeADIFTypeLocation:
                        fieldValue = [QEADIFValue locationFromString:value];
                        break;
                    case qeADIFTypeEnumeration:
                        if ([qslSentFields member:fieldName]) {
                            fieldValue = [fields objectForKey:fieldName];
                            if (nil == fieldValue) {
                                fieldValue = [QEADIFValue 
                                              valueFromString:([value boolValue]
                                                               ? @"Y"
                                                               : @"N")
                                              fromEnumeration:[QEADIFValue 
                                                               enumerationSpecForFieldName:fieldName]];
                            } // if
                            break;
                        } // if
                        
                        if ([qslRcvdFields member:fieldName]) {
                            fieldValue = [fields objectForKey:fieldName];
                            if (nil == fieldValue) {
                                fieldValue = [QEADIFValue 
                                              valueFromString:([value boolValue]
                                                               ? @"Y"
                                                               : @"N")
                                              fromEnumeration:[QEADIFValue 
                                                               enumerationSpecForFieldName:fieldName]];
                            } // if
                            break;
                        } // if
                        
                        fieldValue = [QEADIFValue valueFromString:value
                                                  fromEnumeration:enumSpec];
                        break;
                    default:    // CAN'T HAPPEN
                        NSAssert1(NO,
                                  @"  CAN'T HAPPEN: presented with undefined data type number %ld",
                                  ((long) dataType));
                        break;
                } // switch
                [fields setObject:[[[QEADIFDataField alloc] initWithFieldName:fieldName fieldValue:fieldValue] autorelease] forKey:fieldName];
            } // for
        } // for
        [exportRecords addObject:[fields allValues]];
    } // for
    
    [outFile setAdifRecords:exportRecords];
    [outFile regenerateHeadersWithApplicationName:@"Chalk"];
    
    //-NSLog(@"  output file:\n\n%@\n\n", [outFile dump]);
    
    BOOL result = [outFile writeToFile:filename error:outError];
    [outFile release];
    return result;
} // exportAsADIF:toFilename:error:


#pragma mark DATA MANAGEMENT
+ (BOOL)accessInstanceVariablesDirectly
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return NO;
} // accessInstanceVariablesDirectly

+ (void)initializeTables
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [QEADIFImporter initializeField2ColumnMap];
    [QEADIFImporter initializeColumn2FieldsMap];
    [QEADIFImporter initializeColumnTypeTable];
} // initializeTables

+ (void)initializeField2ColumnMap
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSMutableDictionary *tmp = [[NSMutableDictionary alloc] init];
    
    // Key: ADIF field name; Object: column name
    [tmp setObject:@"band"         forKey:@"BAND"];
    [tmp setObject:@"receive_band" forKey:@"BAND_RX"];
    [tmp setObject:@"callsign"     forKey:@"CALL"];
    [tmp setObject:@"comment"      forKey:@"COMMENT"];
    [tmp setObject:@"freq"         forKey:@"FREQ"];
    [tmp setObject:@"receive_freq" forKey:@"FREQ_RX"];
    [tmp setObject:@"qsl_lotw_rcvd"     forKey:@"LOTW_QSL_RCVD"];
    [tmp setObject:@"qsl_lotw_sent"     forKey:@"LOTW_QSL_SENT"];
    [tmp setObject:@"mode"         forKey:@"MODE"];
    [tmp setObject:@"qsl_rcvd"     forKey:@"QSL_RCVD"];
    [tmp setObject:@"qsl_sent"     forKey:@"QSL_SENT"];
    [tmp setObject:@"qso_date"     forKey:@"QSO_DATE"];
    [tmp setObject:@"rst_rcvd"     forKey:@"RST_RCVD"];
    [tmp setObject:@"rst_sent"     forKey:@"RST_SENT"];
    [tmp setObject:@"qso_date"     forKey:@"TIME_ON"];
    [tmp setObject:@"wattsOut"     forKey:@"TX_PWR"];
    
    field2ColumnMap = [[NSDictionary alloc] initWithDictionary:tmp];
    [tmp release];
} // initializeField2ColumnMap

+ (void)initializeColumn2FieldsMap
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSMutableDictionary *tmp = [[NSMutableDictionary alloc] init];
    
    // Key: column name; Object: ADIF field name(s)
    [tmp setObject:[NSArray arrayWithObject:@"BAND"]     forKey:@"band"];
    [tmp setObject:[NSArray arrayWithObject:@"BAND_RX"]  forKey:@"receive_band"];
    [tmp setObject:[NSArray arrayWithObject:@"CALL"]     forKey:@"callsign"];
    [tmp setObject:[NSArray arrayWithObject:@"COMMENT"]  forKey:@"comment"];
    [tmp setObject:[NSArray arrayWithObject:@"FREQ"]     forKey:@"freq"];
    [tmp setObject:[NSArray arrayWithObject:@"FREQ_RX"]  forKey:@"receive_freq"];
    [tmp setObject:[NSArray arrayWithObject:@"LOTW_QSL_RCVD"] forKey:@"qsl_lotw_rcvd"];
    [tmp setObject:[NSArray arrayWithObject:@"LOTW_QSL_SENT"] forKey:@"qsl_lotw_sent"];
    [tmp setObject:[NSArray arrayWithObject:@"MODE"]     forKey:@"mode"];
    [tmp setObject:[NSArray arrayWithObject:@"QSL_RCVD"] forKey:@"qsl_rcvd"];
    [tmp setObject:[NSArray arrayWithObject:@"QSL_SENT"] forKey:@"qsl_sent"];
    [tmp setObject:[NSArray arrayWithObjects:@"QSO_DATE", @"TIME_ON", nil] forKey:@"qso_date"];
    [tmp setObject:[NSArray arrayWithObject:@"RST_RCVD"] forKey:@"rst_rcvd"];
    [tmp setObject:[NSArray arrayWithObject:@"RST_SENT"] forKey:@"rst_sent"];
    [tmp setObject:[NSArray arrayWithObject:@"TX_PWR"]   forKey:@"wattsOut"];
    
    column2FieldsMap = [[NSDictionary alloc] initWithDictionary:tmp];
    [tmp release];
} // initializeColumn2FieldsMap

+ (void)initializeColumnTypeTable
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSMutableDictionary *tmp = [[NSMutableDictionary alloc] init];
    
    // Key: column name; Object: column type
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"band"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"callsign"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"comment"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnDecimal] forKey:@"freq"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"mode"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnBooleanQslRcvd] forKey:@"qsl_lotw_rcvd"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnBooleanQslSent] forKey:@"qsl_lotw_sent"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnBooleanQslRcvd] forKey:@"qsl_rcvd"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnBooleanQslSent] forKey:@"qsl_sent"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnDate]    forKey:@"qso_date"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"receive_band"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnDecimal] forKey:@"receive_freq"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"rst_rcvd"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnString]  forKey:@"rst_sent"];
    [tmp setObject:[NSNumber numberWithInt:qeKeyColumnDecimal] forKey:@"wattsOut"];
    
    columnTypeTable = [[NSDictionary alloc] initWithDictionary:tmp];
    [tmp release];
} // initializeColumnTypeTable
@end
